라우팅 심화
Route Group(라우트 그룹)
- 여러 관련 경로를 그룹으로 묶어 구조를 더 명확하게 표현
- 폴더 이름에 괄호
()
를 사용하여 URL 경로에 포함되지 않는다
import "../globals.css";
export default function RootLayout({ children }) {
return (
<html lang="en">
<body style={{ width: "100vw", height: "100vh" }}>
<div>{children}</div>
</body>
</html>
);
}
"use client";
import { useRouter } from "next/navigation";
export default function HomePage() {
const router = useRouter();
const handleLinkPage = () => {
router.push("/content");
};
return (
<>
<h1>광고 페이지 입니다.</h1>
<button onClick={handleLinkPage}>광고 닫기</button>
</>
);
}
export default function Layout({ children }) {
return (
<html>
<body>{children}</body>
</html>
);
}
export default function ContentPage() {
return <>내용 페이지 입니다.</>;
}
src/app
├── (advertise)
│ ├── layout.js
│ ├── loading.js
│ └── page.js
└── (main)
├── content
│ └── page.js
└── layout.js
위의 코드는 Next.js를 실행 시킨 후, 광고 페이지 버튼을 눌러 내용 페이지로 이동하게 하는 코드
폴더 구조를 보면 app의 page.js가 없음에도 불구하고 동작이 된다.
(advertise)
와 (main)
에서 advertise에만 page.js가 있다.
Next.js가 판단하여 page.js가 존재하는 라우트 그룹의 페이지를 렌더링 시킨다.
만약 라우트 그룹 중 page.js가 2개이상 있을 경우 에러를 발생
페이지가 렌더링이 되고 경로를 확인해보면 /advertise
로 되어있지 않다.
즉, 괄호()
로 감쌓여진 폴더는 경로를 무시하며, 라우트를 묶기 위한 구조로 활용된다.
대신 동일한 레벨의 두 영역으로 나뉘어질 경우, 각각의 layout.js을 작성해야된다.
광고 닫기
버튼을 누르면 (main)/content
의 page.js화면이 나오는 것을 확인할 수 있다.
Link를 통해 /content
경로를 설정하였음에도 (main)
이 경로에 영향을 끼치지 않는다는 것을 확인
Parallel Routes(병렬 라우트)
- 동일한 경로 레벨에서 여러 개의 라우트가 동시에 활성화되고 관리
- 하나의 레이아웃에 여러 라우트가 동시에 렌더링
- 병렬 라우트를 생성 시, 폴더명을
@폴더명
으로 작성
export default async function Home({ props }) {
return <div>Home Page입니다.</div>;
}
import "./globals.css";
export default function RootLayout({ children, info, content }) {
return (
<html lang="en">
<body style={{ width: "100vw", height: "100vh" }}>
<div>{children}</div>
<div>{info}</div>
<div>{content}</div>
</body>
</html>
);
}
export default function AppLoading() {
return <>로딩 중...</>;
}
export default async function InfoPage() {
await new Promise((resolve) => setTimeout(resolve, 5000));
return <div>Info Page 입니다.</div>;
}
export default async function ContentPage() {
await new Promise((resolve) => setTimeout(resolve, 1000));
return <div>Conetent Page 입니다.</div>;
}
src/app
├── page.js (Home 페이지)
├── loading.js (loading 페이지)
├── layout.js (layout 페이지)
├── @info
│ └── page.js (info 페이지)
└── @content
└── page.js (content 페이지)
app경로에 info
, content
라우팅 페이지를 정의해놓은 코드
info
와 content
가 병렬 라우팅인지 확인하기 위해 setTimeout을 통해 각각 렌더링 시간을 걸어두었다.
병렬 라우팅을 하기 위해서는 동일한 레벨에서 @폴더명
으로 폴더를 만들고 안에 page.js를 생성해야된다.
@ + 폴더명
을 **슬롯(slots)**이라 한다.
병렬 라우팅에 대한 slots 및 page.js가 생성되었다면, 이 병렬 라우팅을 렌더링 시켜야된다.
slots들은 부모의 layout.js
에 접근이 가능하다.
즉, info
와 content
의 부모인 app영역의 layout.js에서 사용할 수 있다는 것이다.
export default function RootLayout({ children, info, content }) {}
layout.js 코드를 보면 props영역에 직접적인 slots명을 작성하면 병렬 라우팅으로 선언된 slots들의 page.js를 호출한다.
실행 후 0초
실행 후 1초 뒤
실행 후 5초 뒤
결과 이미지를 보면 병렬 라우팅을 처리하게 되면 위와 같이 독립적인 로드 및 렌더링이 된다.
즉, 병렬 라우팅은 서로 독립적인 데이터를 로드해야할 경우에 유용하다.
Intercepting Routes(가로채는 라우트)
- 특정 라우트의 접근을 조건에 따라 제어하거나 변경할 수 있는 라우팅 기법
- 주로 모달의 띄우기 목적에 사용
Intercepting Routes 파일 생성 Convention
(.)폴더명
: 동일한 레벨의 세그먼트와 일치(..)폴더명
: 한 레벨 위의 세그먼트와 일치(..)(..)폴더명
: 두 레벨 위의 세그먼트와 일치(...)폴더명
: 루트 단위부터와의 세그먼트와 일치
폴더명은 반드시 Intercept route를 적용할 경로 이름과 똑같이 작성해야된다.
Intercepting Routes 예시 코드
export default function RootLayout({ children, modal }) {
return (
<html lang="en">
<body>
{modal}
{children}
</body>
</html>
);
}
import Link from "next/link";
export default function HomePage() {
return (
<>
<h1>홈페이지 입니다.</h1>
<Link href="/image">이미지 확인하러 가기</Link>
</>
);
}
export default function ModalDefaultPage() {
return null;
}
.modal {
background-color: #bababa;
padding: 2rem;
border-radius: 4px;
border: none;
box-shadow: 0 0 10px 0 #181817;
}
.backdrop {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.85);
display: flex;
justify-content: center;
align-items: center;
}
"use client";
import { useRouter } from "next/navigation";
import classes from "./page.module.css";
export default function InterceptImage() {
const router = useRouter();
return (
<div className={classes.backdrop} onClick={router.back}>
<dialog className={classes.modal} open>
<img src="../favicon.ico" alt="Intercept Image" />
</dialog>
</div>
);
}
export default function ImagePage() {
return <img src="../favicon.ico" />;
}
src/app
├── page.js (Home 페이지)
├── layout.js (layout 페이지)
├── @modal
│ ├── (.)image
│ │ ├── page.module.css (intercepting css)
│ │ └── page.js (intercepting 페이지)
│ └── default.js (default 페이지)
└── image
└── page.js (image 페이지)
위의 코드는 버튼을 누를 경우, 모달창에 이미지를 띄우는 코드
HomePage에서 이미지 확인하러 가기
버튼을 누를 경우, /image
경로로 이동한다.
이 때, image폴더의 page.js가 표현되어야지만, 동작을 해보면 /@modal/(.)image
의 page.js 페이지가 표현된다.
/@modal/(.)image
를 이용하여 Intercept Route를 선언하였기 때문이다.
@modal에 default.js의 용도는 무엇일까?
default.js에는 Next.js에서 사용하는 예약어이다.
기본적으로 해당 경로에 접근할 때 가장 먼저 렌더링되는 Component를 정의
export default function RootLayout({ children, modal }) {
return (
<html lang="en">
<body>
{modal}
{children}
</body>
</html>
);
}
실질적으로 렌더링과 동시에 modal이 동작하는 것이 아닌 특정 버튼을 클릭하였을 때, 해당 URL로 변경되며 modal이 뜨기 때문에
해당 경로에 처음 접근할 때 빈 화면을 렌더링하여 모달이 열리기 전에 기본 상태를 유지하기 위해 default.js를 사용.
image경로에 page.js가 왜 필요한가?
Intercept Route는 경로를 가로채서 특정 Component를 렌더링하는 기능
즉, Link 혹은 useRouter를 이용하게 되면 페이지가 가로채서 Intercept Route로 설정한 페이지가 렌더링된다.
만약, 사용자가 직접적으로 URL을 이용하여 /image
경로를 진입하거나,
/image
경로의 진입 후, 새로고침을 하게 된다면 Intercept Route가 발생하지 않고 /image/page.js
가 렌더링이 된다.
Next.js에서 서버 사이드 렌더링 또는 정적 파일 제공의 원칙에 따르기 때문이다.
왜 Intercepting Route를 이용해서 modal을 표현해야되나?
@modal/(.)image/page.js
의 코드를 image/page.js
에 넣은 후, @modal
폴더를 지우고 실행해보면 된다.
-
Intercept Route 적용 코드
-
Intercept Route 미적용 코드
위의 이미지와 같이 별도의 페이지로 렌더링이 된다.
즉, Intercept Route를 사용하는 이유는 사용자 경험 향상이 있으며, SEO 및 URL 일관성 유지에 매우 중요한 기술이다.
Catch-All Routes
- 동적 경로를 처리하는 방법
- URL의 여러 세그먼트를 하나의 경로로 캡처하여 단일 Component에서 처리할 수 있게 하는 기능
[...폴더명]
으로 폴더를 생성
import Link from "next/link";
export default function HomePage() {
return (
<>
<h1>HomePage 입니다.</h1>
<Link href="/catch">페이지 이동</Link>
</>
);
}
export default function CatchAllRouterPage(params) {
console.log("Catch All Routes 확인", params);
return <h1>Catch All Router Page</h1>;
}
Catch All Routes에 대해 출력물을 확인하기 위한 코드
Catch All Routes를 사용하기 위해서는 [...slug]
라는 폴더를 생성
[]
는 동적 라우팅을 의미하며, ...
은 모든 하위 경로를 캡쳐하는 것을 의미
app의 page.js에서 Link를 통해 /catch
경로로 이동을 한다.
/catch
로 이동 후, params를 출력해보면 동적 라우팅과 똑같이 출력을 한다.
위와 같이 params를 통해 캡쳐된 경로 세그먼트인 catch
가 출력된 것을 보인다.
Catch All Routes는 별도의 폴더를 생성하지 않고도 추가적인 경로도 접근이 가능하다.
/catch/all/routes
라는 경로를 접근
위와 같이 params를 통해 캡쳐된 경로 세그먼트인 catch
, all
,routes
가 출력된 것을 확인할 수 있다.
또한, [...slug]
의 page.js 페이지를 렌더링 하는 것을 확인할 수 있다.
Optional Catch All Routes
- Catch All Routes의 확장 개념
- 지정된 경로 이외에 상위 경로도 캡쳐. 즉, 세그먼트가 없어도 매칭
[[...폴더명]]
으로 폴더를 생성
import Link from "next/link";
export default function HomePage() {
return (
<>
<h1>HomePage 입니다.</h1>
<Link href="/catch">페이지 이동</Link>
</>
);
}
export default function CatchAllRouterPage(params) {
console.log("Optional Catch All Routes 확인", params);
return <h1>Optional Catch All Router Page</h1>;
}
Optional Catch All Routes에 대해 출력물을 확인하기 위한 코드
app의 page.js에서 Link를 통해 /catch
경로로 이동
catch폴더에 page.js를 생성하지 않았음에도 페이지 렌더링이 된다.
렌더링이 된 페이지는 [[...slug]]
에 생성된 page.js이다.
/catch
의 page.js가 없어도 하위 경로의 Optional Catch All Routes의 page.js를 찾아 렌더링한다.
위의 이미지는 /catch
의 경로에 접근했을 때의 출력물
/catch
경로에 접근하였지만 [[...slug]]
의 page.js를 렌더링 하였기 때문에 출력하는 것을 확인할 수 있다.
하지만 [[...slug]]
에 대한 라우팅이 아닌 /catch
에 대한 라우팅이기 때문에 slug에 대한 세그먼트가 []
으로 출력이 된다.
위의 이미지는 /catch/all/routes
의 경로에 접근했을 때의 출력물
/catch
는 Optional Catch All Routes를 통해 []
으로 출력된 것을 확인
이외 /catch
하위 경로의 세그먼트들은 slug에 저장된 것을 확인할 수 있다.
Optional Catch All Router는 최상단 경로에서 [[...폴더명]]
생성이 불가능
반드시 경로를 생성 후, Optional Catch All Router를 생성해야된다.